@@ -74,6 +74,8 @@ gem 'slack-notifier', '~> 0.5.0' |
||
74 | 74 |
|
75 | 75 |
gem 'therubyracer', '~> 0.12.1' |
76 | 76 |
|
77 |
+gem 'mqtt' |
|
78 |
+ |
|
77 | 79 |
group :development do |
78 | 80 |
gem 'binding_of_caller' |
79 | 81 |
gem 'better_errors' |
@@ -160,6 +160,7 @@ GEM |
||
160 | 160 |
mime-types (1.25.1) |
161 | 161 |
mini_portile (0.5.3) |
162 | 162 |
minitest (5.3.3) |
163 |
+ mqtt (0.2.0) |
|
163 | 164 |
multi_json (1.9.3) |
164 | 165 |
multi_xml (0.5.5) |
165 | 166 |
multipart-post (2.0.0) |
@@ -341,6 +342,7 @@ DEPENDENCIES |
||
341 | 342 |
kaminari (~> 0.15.1) |
342 | 343 |
kramdown (~> 1.3.3) |
343 | 344 |
liquid (~> 2.6.1) |
345 |
+ mqtt |
|
344 | 346 |
mysql2 (~> 0.3.15) |
345 | 347 |
nokogiri (~> 1.6.1) |
346 | 348 |
protected_attributes (~> 1.0.7) |
@@ -0,0 +1,138 @@ |
||
1 |
+# encoding: utf-8 |
|
2 |
+require "mqtt" |
|
3 |
+require "json" |
|
4 |
+ |
|
5 |
+module Agents |
|
6 |
+ class MqttAgent < Agent |
|
7 |
+ description <<-MD |
|
8 |
+ The MQTT agent allows both publication and subscription to an MQTT topic. |
|
9 |
+ |
|
10 |
+ MQTT is a generic transport protocol for machine to machine communication. |
|
11 |
+ |
|
12 |
+ You can do things like: |
|
13 |
+ |
|
14 |
+ * Publish to [RabbitMQ](http://www.rabbitmq.com/mqtt.html) |
|
15 |
+ * Run [OwnTracks, a location tracking tool](http://owntracks.org/) for iOS and Android |
|
16 |
+ * Subscribe to your home automation setup like [Ninjablocks](http://forums.ninjablocks.com/index.php?p=/discussion/661/today-i-learned-about-mqtt/p1) or [TheThingSystem](http://thethingsystem.com/dev/supported-things.html) |
|
17 |
+ |
|
18 |
+ Simply choose a topic (think email subject line) to publish/listen to, and configure your service. |
|
19 |
+ |
|
20 |
+ It's easy to setup your own [broker](http://jpmens.net/2013/09/01/installing-mosquitto-on-a-raspberry-pi/) or connect to a [cloud service](www.cloudmqtt.com) |
|
21 |
+ |
|
22 |
+ Hints: |
|
23 |
+ Many services run mqtts (mqtt over SSL) often with a custom certificate. |
|
24 |
+ |
|
25 |
+ You'll want to download their cert and install it locally, specifying the ```certificate_path``` configuration. |
|
26 |
+ |
|
27 |
+ |
|
28 |
+ Example configuration: |
|
29 |
+ |
|
30 |
+ <pre><code>{ |
|
31 |
+ 'uri' => 'mqtts://user:pass@locahost:8883' |
|
32 |
+ 'ssl' => :TLSv1, |
|
33 |
+ 'ca_file' => './ca.pem', |
|
34 |
+ 'cert_file' => './client.crt', |
|
35 |
+ 'key_file' => './client.key', |
|
36 |
+ 'topic' => 'huginn' |
|
37 |
+ } |
|
38 |
+ </code></pre> |
|
39 |
+ |
|
40 |
+ Subscribe to CloCkWeRX's TheThingSystem instance (thethingsystem.com), where |
|
41 |
+ temperature and other events are being published. |
|
42 |
+ |
|
43 |
+ <pre><code>{ |
|
44 |
+ 'uri' => 'mqtt://kcqlmkgx:sVNoccqwvXxE@m10.cloudmqtt.com:13858', |
|
45 |
+ 'topic' => 'the_thing_system/demo' |
|
46 |
+ } |
|
47 |
+ </code></pre> |
|
48 |
+ |
|
49 |
+ Subscribe to all topics |
|
50 |
+ <pre><code>{ |
|
51 |
+ 'uri' => 'mqtt://kcqlmkgx:sVNoccqwvXxE@m10.cloudmqtt.com:13858', |
|
52 |
+ 'topic' => '/#' |
|
53 |
+ } |
|
54 |
+ </code></pre> |
|
55 |
+ |
|
56 |
+ Find out more detail on [subscription wildcards](http://www.eclipse.org/paho/files/mqttdoc/Cclient/wildcard.html) |
|
57 |
+ MD |
|
58 |
+ |
|
59 |
+ event_description <<-MD |
|
60 |
+ Events are simply nested MQTT payloads. For example, an MQTT payload for Owntracks |
|
61 |
+ |
|
62 |
+ <pre><code>{ |
|
63 |
+ "topic": "owntracks/kcqlmkgx/Dan", |
|
64 |
+ "message": {"_type": "location", "lat": "-34.8493644", "lon": "138.5218119", "tst": "1401771049", "acc": "50.0", "batt": "31", "desc": "Home", "event": "enter"}, |
|
65 |
+ "time": 1401771051 |
|
66 |
+ }</code></pre> |
|
67 |
+ MD |
|
68 |
+ |
|
69 |
+ def validate_options |
|
70 |
+ unless options['uri'].present? && |
|
71 |
+ options['topic'].present? |
|
72 |
+ errors.add(:base, "topic and uri are required") |
|
73 |
+ end |
|
74 |
+ end |
|
75 |
+ |
|
76 |
+ def working? |
|
77 |
+ event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? |
|
78 |
+ end |
|
79 |
+ |
|
80 |
+ def default_options |
|
81 |
+ { |
|
82 |
+ 'uri' => 'mqtts://user:pass@locahost:8883', |
|
83 |
+ 'ssl' => :TLSv1, |
|
84 |
+ 'ca_file' => './ca.pem', |
|
85 |
+ 'cert_file' => './client.crt', |
|
86 |
+ 'key_file' => './client.key', |
|
87 |
+ 'topic' => 'huginn', |
|
88 |
+ 'max_read_time' => '10' |
|
89 |
+ } |
|
90 |
+ end |
|
91 |
+ |
|
92 |
+ def mqtt_client |
|
93 |
+ @client ||= MQTT::Client.new(options['uri']) |
|
94 |
+ |
|
95 |
+ if options['ssl'] |
|
96 |
+ @client.ssl = options['ssl'].to_sym |
|
97 |
+ @client.ca_file = options['ca_file'] |
|
98 |
+ @client.cert_file = options['cert_file'] |
|
99 |
+ @client.key_file = options['key_file'] |
|
100 |
+ end |
|
101 |
+ |
|
102 |
+ @client |
|
103 |
+ end |
|
104 |
+ |
|
105 |
+ def receive(incoming_events) |
|
106 |
+ mqtt_client.connect do |c| |
|
107 |
+ incoming_events.each do |event| |
|
108 |
+ c.publish(options['topic'], payload) |
|
109 |
+ end |
|
110 |
+ |
|
111 |
+ c.disconnect |
|
112 |
+ end |
|
113 |
+ end |
|
114 |
+ |
|
115 |
+ |
|
116 |
+ def check |
|
117 |
+ mqtt_client.connect do |c| |
|
118 |
+ |
|
119 |
+ Timeout::timeout((options['max_read_time'].presence || 15).to_i) { |
|
120 |
+ c.get(options['topic']) do |topic, message| |
|
121 |
+ |
|
122 |
+ # A lot of services generate JSON. Try that first |
|
123 |
+ payload = JSON.parse(message) rescue message |
|
124 |
+ |
|
125 |
+ create_event :payload => { |
|
126 |
+ 'topic' => topic, |
|
127 |
+ 'message' => payload, |
|
128 |
+ 'time' => Time.now.to_i |
|
129 |
+ } |
|
130 |
+ end |
|
131 |
+ } rescue TimeoutError |
|
132 |
+ |
|
133 |
+ c.disconnect |
|
134 |
+ end |
|
135 |
+ end |
|
136 |
+ |
|
137 |
+ end |
|
138 |
+end |
@@ -0,0 +1,52 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+require 'mqtt' |
|
3 |
+require './spec/support/fake_mqtt_server' |
|
4 |
+ |
|
5 |
+describe Agents::MqttAgent do |
|
6 |
+ |
|
7 |
+ before :each do |
|
8 |
+ @error_log = StringIO.new |
|
9 |
+ |
|
10 |
+ @server = MQTT::FakeServer.new(41234, '127.0.0.1') |
|
11 |
+ @server.just_one = true |
|
12 |
+ @server.logger = Logger.new(@error_log) |
|
13 |
+ @server.logger.level = Logger::DEBUG |
|
14 |
+ @server.start |
|
15 |
+ |
|
16 |
+ @valid_params = { |
|
17 |
+ 'uri' => "mqtt://#{@server.address}:#{@server.port}", |
|
18 |
+ 'topic' => '/#', |
|
19 |
+ 'max_read_time' => '1', |
|
20 |
+ 'expected_update_period_in_days' => "2" |
|
21 |
+ } |
|
22 |
+ |
|
23 |
+ @checker = Agents::MqttAgent.new( |
|
24 |
+ :name => "somename", |
|
25 |
+ :options => @valid_params, |
|
26 |
+ :schedule => "midnight", |
|
27 |
+ ) |
|
28 |
+ @checker.user = users(:jane) |
|
29 |
+ @checker.save! |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ after :each do |
|
33 |
+ @server.stop |
|
34 |
+ end |
|
35 |
+ |
|
36 |
+ describe "#check" do |
|
37 |
+ it "should check that initial run creates an event" do |
|
38 |
+ expect { @checker.check }.to change { Event.count }.by(2) |
|
39 |
+ end |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ describe "#working?" do |
|
43 |
+ it "checks if its generating events as scheduled" do |
|
44 |
+ @checker.should_not be_working |
|
45 |
+ @checker.check |
|
46 |
+ @checker.reload.should be_working |
|
47 |
+ three_days_from_now = 3.days.from_now |
|
48 |
+ stub(Time).now { three_days_from_now } |
|
49 |
+ @checker.should_not be_working |
|
50 |
+ end |
|
51 |
+ end |
|
52 |
+end |
@@ -0,0 +1,137 @@ |
||
1 |
+#!/usr/bin/env ruby |
|
2 |
+# |
|
3 |
+# This is a 'fake' MQTT server to help with testing client implementations |
|
4 |
+# |
|
5 |
+# See https://github.com/njh/ruby-mqtt/blob/master/spec/fake_server.rb |
|
6 |
+# |
|
7 |
+# It behaves in the following ways: |
|
8 |
+# * Responses to CONNECT with a successful CONACK |
|
9 |
+# * Responses to PUBLISH by echoing the packet back |
|
10 |
+# * Responses to SUBSCRIBE with SUBACK and a PUBLISH to the topic |
|
11 |
+# * Responses to PINGREQ with PINGRESP |
|
12 |
+# * Responses to DISCONNECT by closing the socket |
|
13 |
+# |
|
14 |
+# It has the following restrictions |
|
15 |
+# * Doesn't deal with timeouts |
|
16 |
+# * Only handles a single connection at a time |
|
17 |
+# |
|
18 |
+ |
|
19 |
+$:.unshift File.dirname(__FILE__)+'/../lib' |
|
20 |
+ |
|
21 |
+require 'logger' |
|
22 |
+require 'socket' |
|
23 |
+require 'mqtt' |
|
24 |
+ |
|
25 |
+ |
|
26 |
+class MQTT::FakeServer |
|
27 |
+ attr_reader :address, :port |
|
28 |
+ attr_reader :last_publish |
|
29 |
+ attr_reader :thread |
|
30 |
+ attr_reader :pings_received |
|
31 |
+ attr_accessor :just_one |
|
32 |
+ attr_accessor :logger |
|
33 |
+ |
|
34 |
+ # Create a new fake MQTT server |
|
35 |
+ # |
|
36 |
+ # If no port is given, bind to a random port number |
|
37 |
+ # If no bind address is given, bind to localhost |
|
38 |
+ def initialize(port=nil, bind_address='127.0.0.1') |
|
39 |
+ @port = port |
|
40 |
+ @address = bind_address |
|
41 |
+ end |
|
42 |
+ |
|
43 |
+ # Get the logger used by the server |
|
44 |
+ def logger |
|
45 |
+ @logger ||= Logger.new(STDOUT) |
|
46 |
+ end |
|
47 |
+ |
|
48 |
+ # Start the thread and open the socket that will process client connections |
|
49 |
+ def start |
|
50 |
+ @socket ||= TCPServer.new(@address, @port) |
|
51 |
+ @address = @socket.addr[3] |
|
52 |
+ @port = @socket.addr[1] |
|
53 |
+ @thread ||= Thread.new do |
|
54 |
+ logger.info "Started a fake MQTT server on #{@address}:#{@port}" |
|
55 |
+ loop do |
|
56 |
+ # Wait for a client to connect |
|
57 |
+ client = @socket.accept |
|
58 |
+ @pings_received = 0 |
|
59 |
+ handle_client(client) |
|
60 |
+ break if just_one |
|
61 |
+ end |
|
62 |
+ end |
|
63 |
+ end |
|
64 |
+ |
|
65 |
+ # Stop the thread and close the socket |
|
66 |
+ def stop |
|
67 |
+ logger.info "Stopping fake MQTT server" |
|
68 |
+ @socket.close unless @socket.nil? |
|
69 |
+ @socket = nil |
|
70 |
+ |
|
71 |
+ @thread.kill if @thread and @thread.alive? |
|
72 |
+ @thread = nil |
|
73 |
+ end |
|
74 |
+ |
|
75 |
+ # Start the server thread and wait for it to finish (possibly never) |
|
76 |
+ def run |
|
77 |
+ start |
|
78 |
+ begin |
|
79 |
+ @thread.join |
|
80 |
+ rescue Interrupt |
|
81 |
+ stop |
|
82 |
+ end |
|
83 |
+ end |
|
84 |
+ |
|
85 |
+ |
|
86 |
+ protected |
|
87 |
+ |
|
88 |
+ # Given a client socket, process MQTT packets from the client |
|
89 |
+ def handle_client(client) |
|
90 |
+ loop do |
|
91 |
+ packet = MQTT::Packet.read(client) |
|
92 |
+ logger.debug packet.inspect |
|
93 |
+ |
|
94 |
+ case packet |
|
95 |
+ when MQTT::Packet::Connect |
|
96 |
+ client.write MQTT::Packet::Connack.new(:return_code => 0) |
|
97 |
+ when MQTT::Packet::Publish |
|
98 |
+ client.write packet |
|
99 |
+ @last_publish = packet |
|
100 |
+ when MQTT::Packet::Subscribe |
|
101 |
+ client.write MQTT::Packet::Suback.new( |
|
102 |
+ :message_id => packet.message_id, |
|
103 |
+ :granted_qos => 0 |
|
104 |
+ ) |
|
105 |
+ topic = packet.topics[0][0] |
|
106 |
+ client.write MQTT::Packet::Publish.new( |
|
107 |
+ :topic => topic, |
|
108 |
+ :payload => "hello #{topic}", |
|
109 |
+ :retain => true |
|
110 |
+ ) |
|
111 |
+ client.write MQTT::Packet::Publish.new( |
|
112 |
+ :topic => topic, |
|
113 |
+ :payload => "did you know about #{topic}", |
|
114 |
+ :retain => true |
|
115 |
+ ) |
|
116 |
+ |
|
117 |
+ when MQTT::Packet::Pingreq |
|
118 |
+ client.write MQTT::Packet::Pingresp.new |
|
119 |
+ @pings_received += 1 |
|
120 |
+ when MQTT::Packet::Disconnect |
|
121 |
+ client.close |
|
122 |
+ break |
|
123 |
+ end |
|
124 |
+ end |
|
125 |
+ |
|
126 |
+ rescue MQTT::ProtocolException => e |
|
127 |
+ logger.warn "Protocol error, closing connection: #{e}" |
|
128 |
+ client.close |
|
129 |
+ end |
|
130 |
+ |
|
131 |
+end |
|
132 |
+ |
|
133 |
+if __FILE__ == $0 |
|
134 |
+ server = MQTT::FakeServer.new(MQTT::DEFAULT_PORT) |
|
135 |
+ server.logger.level = Logger::DEBUG |
|
136 |
+ server.run |
|
137 |
+end |